/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.web.gwt.basics.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.genia.toolbox.web.gwt.basics.client.exception.GwtBundledException;
import com.genia.toolbox.web.gwt.basics.client.i18n.GwtI18nMessage;
import com.genia.toolbox.web.gwt.basics.client.i18n.I18nArrayCallBack;
import com.genia.toolbox.web.gwt.basics.client.i18n.I18nCallBack;
import com.genia.toolbox.web.gwt.basics.client.log.Logger;
import com.genia.toolbox.web.gwt.basics.client.log.Logger.Level;
import com.genia.toolbox.web.gwt.basics.client.log.impl.AlertLogger;
import com.genia.toolbox.web.gwt.basics.client.log.impl.NullLogger;
import com.genia.toolbox.web.gwt.basics.client.message.Messages;
import com.genia.toolbox.web.gwt.basics.client.service.GwtFileUploadService;
import com.genia.toolbox.web.gwt.basics.client.service.GwtFileUploadServiceAsync;
import com.genia.toolbox.web.gwt.basics.client.service.I18nService;
import com.genia.toolbox.web.gwt.basics.client.service.I18nServiceAsync;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.xml.client.Node;

/**
 * Helper class for GWT.
 */
public final class GwtHelper
{
  /**
   * a static {@link Map} containing the parameters of the request.
   */
  private static Map<String, String> paramMap;

  /**
   * {@link I18nCallBack} that is used to translate one element for an
   * {@link I18nArrayCallBack}.
   */
  private static class I18nArrayCallBackHelper
      implements I18nCallBack
  {

    /**
     * the callback to call once the translation is done.
     */
    private Command callback;

    /**
     * the index of the messages to fill.
     */
    private int index;

    /**
     * the messages that must be filled with the result.
     */
    private String[] messages;



    /**
     * constructor.
     * 
     * @param messages
     *          the messages that must be filled with the result
     * @param index
     *          the index of the messages to fill
     * @param callback
     *          the callback to call once the translation is done
     */
    public I18nArrayCallBackHelper(String[] messages, int index, Command callback)
    {
      this.messages = messages;
      this.index = index;
      this.callback = callback;
    }



    /**
     * the callback called when the message has been successfully translated.
     * 
     * @param message
     *          the value of the message.
     * @see com.genia.toolbox.web.gwt.basics.client.i18n.I18nCallBack#getMessage(java.lang.String)
     */
    public void getMessage(String message)
    {
      messages[index] = message;
      callback.execute();
    }

  }

  /**
   * the <code>I18nService</code> to use.
   */
  private static GwtFileUploadServiceAsync gwtFileUploadService = null;

  /**
   * the <code>I18nService</code> to use.
   */
  private static I18nServiceAsync i18nService = null;

  /**
   * the static logger used by the application. Changing it allows to change the
   * location of the logs.
   */
  public static Logger logger = new AlertLogger();

  /**
   * cashing messages.
   */
  private static final Map<String, String> MESSAGES_BY_KEY = new HashMap<String, String>();

  /**
   * the number of successfull submits (for allowing History.back() to work).
   */
  private static int nbSucessfullSubmits = 0;



  /**
   * add 1 to the number of sucessfull submits.
   */
  public static void addSucessfullSubmit()
  {
    nbSucessfullSubmits++;
  }



  /**
   * Add an element to a listbox with a translated message.
   * 
   * @param listBox
   *          the listbox widget which we set the text
   * @param label
   *          the label from which we get the message
   * @param value
   *          the value of the item to add
   */
  public static void addToListBox(final ListBox listBox, final GwtI18nMessage label, final String value)
  {
    listBox.addItem("", value);
    fillListBox(listBox, label, listBox.getItemCount() - 1);
  }



  /**
   * log all exception trhough the logger.
   */
  public static void addUncaughtExceptionHandler()
  {
    GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler()
    {

      public void onUncaughtException(final Throwable cause)
      {
        GwtHelper.error(new GwtI18nMessage(Messages.UNCAUGHT_EXCEPTION_ERROR_KEY, new String[] { cause.toString() }), cause);
      }

    });

  }



  /**
   * write the translation of a <code>GwtI18nMessage</code> in an alert window.
   * 
   * @param message
   *          the message to translate
   */
  public static void error(final GwtI18nMessage message)
  {
    log(message, Level.ERROR);
  }



  /**
   * write the translation of a <code>Throwable</code> in an alert window. If
   * the throwable is a <code>GwtBundledException</code> it writes the
   * translation of the exception, otherwise it writes the default message.
   * 
   * @param defaultMessage
   *          default message if cause is not a <code>GwtBundledException</code>
   * @param cause
   *          the exception to display
   */
  public static void error(final GwtI18nMessage defaultMessage, final Throwable cause)
  {
    GwtI18nMessage message = defaultMessage;
    if (cause instanceof GwtBundledException) {
      final GwtBundledException gwtException = (GwtBundledException) cause;
      message = gwtException.getGwtI18nMessage();
    }
    error(message);
  }



  /**
   * Add the label of an element of a ListBox.
   * 
   * @param listBox
   *          the listbox widget which we set the text
   * @param label
   *          the label from which we get the message
   * @param index
   *          the index of the item to set
   */
  public static void fillListBox(final ListBox listBox, final GwtI18nMessage label, final int index)
  {
    getMessage(label, new I18nCallBack()
    {
      public void getMessage(final String message)
      {
        listBox.setItemText(index, message);
      }
    });
  }



  /**
   * fill the text of a widget with the translated value of a
   * <code>GwtI18nMessage</code>.
   * 
   * @param widget
   *          the widget to fill
   * @param message
   *          the message to translate
   */
  public static void fillText(final HasText widget, final GwtI18nMessage message)
  {
    getMessage(message, new I18nCallBack()
    {

      public void getMessage(final String message)
      {
        widget.setText(message);
      }

    });
  }



  /**
   * fill the title of a widget with the translated value of a
   * <code>GwtI18nMessage</code>.
   * 
   * @param uiObject
   *          the object to fill
   * @param message
   *          the message to translate
   */
  public static void fillTitle(final UIObject uiObject, final GwtI18nMessage message)
  {
    getMessage(message, new I18nCallBack()
    {

      public void getMessage(final String message)
      {
        uiObject.setTitle(message);
      }

    });
  }



  /**
   * returns the context path.
   * 
   * @return the context path
   */
  public static String getContextPath()
  {
    if (!GWT.isScript()) {
      return "";
    }
    return DOM.getElementProperty(DOM.getElementById("contextPath"), "value");
  }



  /**
   * returns the correct URL (adding the prefix path if needed) for the given
   * URL.
   * 
   * @param url
   *          the base URL to transform
   * @return the correct URL corresponding to the given one
   */
  public static String getCorrectUlr(String url)
  {
    if (url.startsWith("/")) {
      return getContextPath() + url;
    }
    return url;
  }



  /**
   * returns the <code>GwtFileUploadService</code> to use.
   * 
   * @return the <code>GwtFileUploadService</code> to use
   */
  public static GwtFileUploadServiceAsync getGwtFileUploadService()
  {
    synchronized (GwtHelper.class) {
      if (gwtFileUploadService == null) {
        gwtFileUploadService = (GwtFileUploadServiceAsync) GWT.create(GwtFileUploadService.class);
        final ServiceDefTarget endpoint = (ServiceDefTarget) gwtFileUploadService;
        endpoint.setServiceEntryPoint(GwtHelper.getServiceName(Constants.MODULE_NAME, GwtFileUploadService.SERVICE_NAME));
      }
    }
    return gwtFileUploadService;
  }



  /**
   * returns the <code>I18nService</code> to use.
   * 
   * @return the <code>I18nService</code> to use
   */
  public static I18nServiceAsync getI18nService()
  {
    synchronized (GwtHelper.class) {
      if (i18nService == null) {
        i18nService = (I18nServiceAsync) GWT.create(I18nService.class);
        final ServiceDefTarget endpoint = (ServiceDefTarget) i18nService;
        endpoint.setServiceEntryPoint(GwtHelper.getServiceName(Constants.MODULE_NAME, I18nService.SERVICE_NAME));
      }
    }
    return i18nService;
  }



  /**
   * Call the i18n service.
   * 
   * @param i18nMessage
   *          the message to translate
   * @param callback
   *          the callback
   */
  public static void getMessage(final GwtI18nMessage i18nMessage, final I18nCallBack callback)
  {
    if (i18nMessage == null) {
      return;
    }
    if (i18nMessage.getParameters().length == 0) {
      String translatedMessage = MESSAGES_BY_KEY.get(i18nMessage.getKey());
      if (translatedMessage != null) {
        callback.getMessage(translatedMessage);
      }
      else {

        I18nAggregator.translate(i18nMessage, new I18nCallBack()
        {

          public void getMessage(String message)
          {
            MESSAGES_BY_KEY.put(i18nMessage.getKey(), message);
            callback.getMessage(message);
          }

        });
      }
    }
    else {
      I18nAggregator.translate(i18nMessage, callback);
    }
  }



  /**
   * returns the value of a parameter passed to the Modules as a GET method.
   * 
   * @param name
   *          the name of the parameter.
   * @return the value of the parameter or null if there is no parameter by that
   *         name.
   */
  public static String getParameter(final String name)
  {
    ensureParameterMap();
    return paramMap.get(name);
  }



  /**
   * ensure that that the paramMap static variable is initialized.
   */
  private static void ensureParameterMap()
  {
    if (paramMap == null) {
      paramMap = new HashMap<String, String>();
      String queryString = Window.Location.getQueryString();
      if (queryString != null && queryString.length() > 1) {
        String qs = queryString.substring(1);
        for (String kvPair : qs.split("&")) {
          String[] kv = kvPair.split("=", 2);
          if (kv.length > 1) {
            paramMap.put(kv[0], URL.decodeComponent(kv[1]));
          }
          else {
            paramMap.put(kv[0], "");
          }
        }
      }
    }
  }



  /**
   * Returns the right name to use as argument to
   * <code>ServiceDefTarget.setServiceEntryPoint</code> for a GWT module knowing
   * the base name of a service.
   * 
   * @param moduleName
   *          the name of the GWT module containing the service to call.
   * @param baseName
   *          The base name of the service searched.
   * @return The name to use in
   *         <code>ServiceDefTarget.setServiceEntryPoint</code>
   */
  public static String getServiceName(final String moduleName, final String baseName)
  {
    if (!GWT.isScript() || Boolean.valueOf(getParameter("gwtShell")).booleanValue()) {
      return "/" + GWT.getModuleName() + "/" + baseName;
    }
    final String contextPath = getContextPath();
    return contextPath + "/gwtRpc";
  }



  /**
   * return to the page that brings us to this GWT page. We need to count the
   * number of submit to a hidden iframe because this counts as a history token.
   */
  public static void goBack()
  {
    // Disable log, because a back can cause RPC interruptions and errors.
    disableLogging();
    travelHistory(-(nbSucessfullSubmits + 1));
  }



  /**
   * disable the logger.
   */
  private static void disableLogging()
  {
    logger = new NullLogger();
  }



  /**
   * write the translation of a <code>GwtI18nMessage</code> in an alert window.
   * 
   * @param message
   *          the message to translate
   * @param level
   *          the log level of this message
   */
  public static void log(final GwtI18nMessage message, final Level level)
  {
    getMessage(message, new I18nCallBack()
    {
      public void getMessage(final String message)
      {
        logger.log(message, level);
      }
    });
  }



  /**
   * redirect to the url given in parameter.
   * 
   * @param url
   *          the url to redirect to
   */
  public static void redirect(String url)
  {
    // Disable log, because a back can cause RPC interruptions and errors.
    disableLogging();
    nativeRedirect(url);
  }



  /**
   * redirect to the url given in parameter.
   * 
   * @param url
   *          the url to redirect to
   */
  public native static void nativeRedirect(String url) /*-{
    $wnd.top.location.assign(url);
  }-*/;



  /**
   * retrieve only text content from an XML node.
   * 
   * @param node
   *          the node to retrieve the text node from
   * @return a {@link String} containing the concatenation of all text node of
   *         the root node
   */
  public static String removeMarkup(Node node)
  {
    if (node.getNodeType() == Node.TEXT_NODE) {
      return node.getNodeValue();
    }
    else {
      String res = "";
      for (int i = 0; i < node.getChildNodes().getLength(); i++) {
        res += removeMarkup(node.getChildNodes().item(i));
      }
      return res;
    }
  }



  /**
   * helper method to translate an array of {@link GwtI18nMessage}.
   * 
   * @param messages
   *          the array to translate
   * @param callback
   *          the {@link I18nArrayCallBack} to pass the result to
   */
  public static void translateArray(final GwtI18nMessage[] messages, final I18nArrayCallBack callback)
  {
    final String[] resultMessages = new String[messages.length];

    Command c = new Command()
    {

      int i = 0;



      public void execute()
      {
        i++;
        if (i == messages.length) {
          callback.getMessages(resultMessages);
        }
      }

    };

    for (int i = 0; i < messages.length; i++) {
      getMessage(messages[i], new I18nArrayCallBackHelper(resultMessages, i, c));
    }
  }



  /**
   * travel in the history of the browser.
   * 
   * @param nbPage
   *          the number of page to go (negative number means going back,
   *          positive going forward
   */
  public native static void travelHistory(int nbPage) /*-{
    $wnd.history.go(nbPage);
  }-*/;



  /**
   * private constructor.
   */
  private GwtHelper()
  {
    // Do nothing
  }



  /**
   * Returns a {@link NodeList} of all the {@link Element Elements} with a given
   * class name in the order in which they are encountered in a preorder
   * traversal of the document tree.
   * 
   * @param tagName
   *          the name of the tag to match on (the special value
   *          <code>"*"</code> matches all tags)
   * @param className
   *          the class to match on
   * @return a list containing all the matched elements
   */
  public static List<Element> getElementsByClassName(String tagName, String className)
  {
    List<Element> res = new ArrayList<Element>();
    NodeList<Element> elements = Document.get().getElementsByTagName(tagName);
    for (int i = 0; i < elements.getLength(); i++) {
      Element element = elements.getItem(i);
      if (className.equals(element.getClassName())) {
        res.add(element);
      }
    }
    return res;
  }
}
